db/query
資料夾中創建一個新檔案名為user.sql
。從account.sql
檔案中複製以下查詢:
-- name: CreateAccount :one
INSERT INTO accounts (
owner,
balance,
currency
) VALUES (
$1, $2, $3
) RETURNING *;
-- name: GetAccount :one
SELECT * FROM accounts
WHERE id = $1 LIMIT 1;
將其貼到新建立的user.sql
檔案中。
改變CreateAccount
函數名稱為CreateUser
。
修改表格名稱為users
。
更新欄位名為:username
, hashed_password
, full_name
, 和 email
。
不需指定password_changed_at
和created_at
欄位,因為Postgres會自動以預設值填充這些欄位。
在值列表中添加一個額外的參數,如下:
-- name: CreateUser :one
INSERT INTO users (
username,
hashed_password,
full_name,
email
) VALUES (
$1, $2, $3, $4
) RETURNING *;
將GetAccount
函數名稱改為GetUser
。
查詢更改為從users
表中選擇,並根據username
獲取用戶資訊,如下:
-- name: GetUser :one
SELECT * FROM users
WHERE username = $1 LIMIT 1;
開啟終端機並運行以下命令以生成 Golang 代碼:
❯ make sqlc
sqlc generate
回到Visual Studio Code,你會發現db/sqlc/models.go
文件中新增了一個新的User
結構。
會看到一個新檔案db/sqlc/user.sql.go
,其中包含了兩個函數,用於從資料庫建立和獲取用戶。
user_test.go
檔案db/sqlc
資料夾中創建一個新檔案名為user_test.go
。create
和get Account
編寫的測試,然後將它們貼到此檔案中。createRandomUser
函數將函數名稱更改為createRandomUser
。
設置參數類型為CreateUserParams
。
函數實作如下:
func createRandomUser(t *testing.T) User {
arg := CreateUserParams{
Username: util.RandomOwner(),
HashedPassword: "secret",
FullName: util.RandomOwner(),
Email: util.RandomEmail(),
}
...
}
RandomEmail
函數在util/random.go
檔案中實現一個新的RandomEmail
函數,如下:
// RandomEmail generates a random email
func RandomEmail() string {
return fmt.Sprintf("%s@email.com", RandomString(6))
}
此函數會返回一個隨機生成的電子郵件地址。
createRandomUser
函數回到測試檔案,更改函數調用為testQueries.CreateUser()
,並檢查輸出結果是否為一個用戶物件,函數實作如下:
func createRandomUser(t *testing.T) User {
...
user, err := testQueries.CreateUser(context.Background(), arg)
require.NoError(t, err)
require.NotEmpty(t, user)
require.Equal(t, arg.Username, user.Username)
require.Equal(t, arg.HashedPassword, user.HashedPassword)
require.Equal(t, arg.FullName, user.FullName)
require.Equal(t, arg.Email, user.Email)
require.NotZero(t, user.CreatedAt)
require.True(t, user.PasswordChangedAt.IsZero())
return user
}
這裡我們比較輸出用戶的每個欄位與輸入參數,並驗證CreatedAt
和PasswordChangedAt
欄位的值。
確認 user.PasswordChangedAt
初始為zero timestamp
(使用 IsZero()
函數進行檢查)。
TestCreateUser測試函數只需調用createRandomUser
函數,如下:
func TestCreateUser(t *testing.T) {
createRandomUser(t)
}
接著為GetUser功能編寫測試,如下:
func TestGetUser(t *testing.T) {
user1 := createRandomUser(t)
user2, err := testQueries.GetUser(context.Background(), user1.Username)
require.NoError(t, err)
require.NotEmpty(t, user2)
require.Equal(t, user1.Username, user2.Username)
require.Equal(t, user1.HashedPassword, user2.HashedPassword)
require.Equal(t, user1.FullName, user2.FullName)
require.Equal(t, user1.Email, user2.Email)
require.WithinDuration(t, user1.PasswordChangedAt, user2.PasswordChangedAt, time.Second)
require.WithinDuration(t, user1.CreatedAt, user2.CreatedAt, time.Second)
}
在這裡,我們確保查詢的輸出(user2
)與輸入(user1
)匹配。
首先運行TestCreateUser
測試。
Running tool: /usr/local/go/bin/go test -timeout 30s -run ^TestCreateUser$ github.com/Kcih4518/simpleBank_2023/db/sqlc
ok github.com/Kcih4518/simpleBank_2023/db/sqlc 0.378s
接著運行TestGetUser
測試。
Running tool: /usr/local/go/bin/go test -timeout 30s -run ^TestGetUser$ github.com/Kcih4518/simpleBank_2023/db/sqlc
ok github.com/Kcih4518/simpleBank_2023/db/sqlc
最後使用TablePlus看數據庫中的User
Table的Records
。
在db/sqlc/account_test.go
文件中,當前代碼僅生成一個隨機擁有者,並未與任何現有用戶建立連接。
func createRandomAccount(t *testing.T) Account {
arg := CreateAccountParams{
Owner: util.RandomOwner(),
Balance: util.RandomMoney(),
Currency: util.RandomCurrency(),
}
...
}
為了修復此問題,我們首先需要在數據庫中創建一名用戶。然後,我們會使用新創建的用戶的用戶名作為賬戶擁有者,而不是一個隨機擁有者。
func createRandomAccount(t *testing.T) Account {
user := createRandomUser(t)
arg := CreateAccountParams{
Owner: user.Username,
Balance: util.RandomMoney(),
Currency: util.RandomCurrency(),
}
...
}
run package tests
Running tool: /usr/local/go/bin/go test -timeout 30s -coverprofile=/var/folders/sk/r57q522d46v1h2f7xv98l7mh0000gn/T/vscode-goCJj4SZ/go-code-cover github.com/Kcih4518/simpleBank_2023/db/sqlc
ok github.com/Kcih4518/simpleBank_2023/db/sqlc 0.654s coverage: 80.8% of statements
但是當執行API testing 時會出現以下錯誤:
MockStore
未實現db.Store
介面。它缺少一些功能的實現。這是因為我們之前運行make sqlc
生成代碼時,CreateUser
和GetUser
兩個新函數已被添加到Querier
**介面。而介面是介面的一部分。api/account.go:1: : # github.com/Kcih4518/simpleBank_2023/api [github.com/Kcih4518/simpleBank_2023/api.test]
api/account_test.go:143:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser)
api/account_test.go:233:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser)
api/account_test.go:343:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser)
api/account_test.go:427:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser)
api/account_test.go:544:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser)
api/transfer_test.go:209:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser) (typecheck)
Solution:
重新生成MockStore代碼
命令:
shCopy code
❯ make mock
mockgen -package mockdb -destination db/mock/store.go github.com/techschool/simplebank/db/sqlc Store
檢查新生成的代碼
在**db/mock/store.go
文件中,您可以看到已添加GetUser
和CreateUser
**函數的實現。
**CreateUser
**函數模擬和預期呼叫的代碼結構如下:
goCopy code
// CreateUser mocks base method
func (m *MockStore) CreateUser(arg0 context.Context, arg1 db.CreateUserParams) (db.User, error) {
...
}
// CreateUser indicates an expected call of CreateUser
func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call {
...
}
**GetUser
**函數模擬和預期呼叫的代碼結構如下:
goCopy code
// GetUser mocks base method
func (m *MockStore) GetUser(arg0 context.Context, arg1 string) (db.User, error) {
...
}
// GetUser indicates an expected call of GetUser
func (mr *MockStoreMockRecorder) GetUser(arg0, arg1 interface{}) *gomock.Call {
...
}
重新運行API單元測試
現在API單元測試應該可以運行。讓我們在終端中重新運行**make test
**來確認。
make test
伺服器運行後,將在8080端口上監聽和服務HTTP請求。然後,使用Postman測試現有的API以創建一個新的帳戶。
make server
go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createAccount-fm (3 handlers)
[GIN-debug] GET /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).getAccount-fm (3 handlers)
[GIN-debug] DELETE /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).delAccount-fm (3 handlers)
[GIN-debug] GET /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).listAccount-fm (3 handlers)
[GIN-debug] PATCH /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).updateAccount-fm (3 handlers)
[GIN-debug] POST /transfers --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createTransfer-fm (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on 0.0.0.0:8080
首次測試將嘗試為不存在於資料庫中的擁有者創建一個帳戶。
遇到的錯誤
在api/account.go
文件的創建帳戶處理程序中,如果store.CreateAccount
調用返回錯誤,我們將嘗試將其轉換為pq.Error
類型,並將結果分配給pqErr
變量。如果轉換成功,我們將在此處打印日誌以查看此錯誤的代碼名稱:
func (server *Server) createAccount(ctx *gin.Context) {
...
account, err := server.store.CreateAccount(ctx, arg)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok {
log.Println(pqErr.Code.Name())
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
ctx.JSON(http.StatusOK, account)
}
接著,重新啟動服務器,並通過Postman重新發送相同的請求。
foreign_key_violation
:當我們嘗試為不存在的用戶創建新帳戶時出現。
make server
go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createAccount-fm (3 handlers)
[GIN-debug] GET /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).getAccount-fm (3 handlers)
[GIN-debug] DELETE /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).delAccount-fm (3 handlers)
[GIN-debug] GET /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).listAccount-fm (3 handlers)
[GIN-debug] PATCH /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).updateAccount-fm (3 handlers)
[GIN-debug] POST /transfers --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createTransfer-fm (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on 0.0.0.0:8080
2023/09/19 19:45:08 foreign_key_violation
[GIN] 2023/09/19 - 19:45:08 | 500 | 13.885157ms | ::1 | POST "/accounts"
unique_violation
:當我們嘗試為同一擁有者創建超過一個具有相同貨幣的帳戶時出現。
make server
go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createAccount-fm (3 handlers)
[GIN-debug] GET /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).getAccount-fm (3 handlers)
[GIN-debug] DELETE /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).delAccount-fm (3 handlers)
[GIN-debug] GET /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).listAccount-fm (3 handlers)
[GIN-debug] PATCH /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).updateAccount-fm (3 handlers)
[GIN-debug] POST /transfers --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createTransfer-fm (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on 0.0.0.0:8080
2023/09/19 19:49:10 unique_violation
[GIN] 2023/09/19 - 19:49:10 | 500 | 18.354167ms | ::1 | POST "/accounts"
現在,我們將更新錯誤處理代碼以處理這些特定錯誤情況。我們將使用一個簡單的switch case語句來檢查錯誤代碼名稱,並根據情況返回相應的HTTP狀態代碼:
func (server *Server) createAccount(ctx *gin.Context) {
...
account, err := server.store.CreateAccount(ctx, arg)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code.Name() {
case "foreign_key_violation", "unique_violation":
ctx.JSON(http.StatusForbidden, errorResponse(err))
return
}
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
ctx.JSON(http.StatusOK, account)
}
403 Forbidden
:
{
"error": "pq: insert or update on table \"accounts\" violates foreign key constraint \"accounts_owner_fkey\""
}
403 Forbidden
{
"error": "pq: duplicate key value violates unique constraint \"accounts_owner_currency_idx\""
}